Read Buf

Read Buf

理解 Rust 中的所有权模型

本文是对 Rust 所谓“所有权”概念的高层次介绍,涵盖了以下几个方面:

  • 栈和堆
  • 什么是所有权
  • 什么是借用

如果你想深入了解所有权的主题,我推荐以下资源:

  • 《The Rust Book - Understanding Ownership》
  • 《Programming Rust》

Rust 作为一种语言的主要吸引力之一在于,它在没有垃圾回收器或运行时的情况下提供了内存和线程的安全性。Rust 通过一种所有权模型实现这一点,编译器在编译时强制执行该模型以确保线程和内存的安全。

在 Rust 中,所有权是一套由编译器用来管理内存的规则。这些规则确保内存的使用是安全且高效的。对于某些程序员来说,所有权的概念可能难以理解。但随着经验的积累,遵守这些规则编写代码会变得更加自然。需要注意的是,所有权系统与栈和堆密切相关,这两者是以不同方式使用的不同内存区域。

在此创新之前,通常有两种类型的语言:手动或自动内存管理语言。而现在,有了 Rust,程序员可以在没有运行时的情况下实现内存安全——两全其美。

栈 vs 堆

栈和堆都是程序使用的内存区域。栈存储函数调用帧和局部变量,按后进先出(LIFO)原则操作。存储在栈上的变量在函数退出时会自动释放。由于栈上数据的访问只是指针的移动,因此访问速度快且高效。

堆上的数据可以随机访问,访问效率不如栈,因为需要跟踪指针。另一方面,堆用于动态内存分配,可以存储生命周期比函数调用更长的变量。堆分配的变量必须手动释放。

一般来说,具有固定和已知大小的值存储在栈上,而具有可变或未知大小的值存储在堆上。访问栈上的数据比访问堆上的数据更快,而在堆上分配空间比在栈上推入数据更昂贵。

在手动内存管理语言(如 C 和 C++)中,程序员负责在程序中手动分配和释放内存。这使得程序员可以通过仅在必要时在堆上分配内存来编写更高效的代码。然而,编程错误可能导致程序崩溃或内存泄漏。另一方面,像 Java 和 Python 这样的语言为大多数函数调用在堆上分配内存。由于复制这些数据的代价更高,程序效率较低,但程序员不需要担心内存的分配和释放。相反,垃圾收集器会在某些间隔自动清理程序不再使用的内存。

什么是所有权系统?

Rust 的所有权系统通过强制程序员遵循一套规则,在没有垃圾收集器和内存安全错误风险的情况下管理栈和堆。以下是这些确保内存使用安全高效的规则:

1. 每个值都有一个所有者:

Rust 程序中的每个值都与一个被视为该值的所有者的变量相关联。当这个变量超出作用域时,该值被丢弃,内存被释放。

例如:

let x = 42; // x 是值 42 的所有者
let y = x; // y 现在是值 42 的所有者

2. 只能有一个所有者

每个值一次只能有一个所有者,确保没有多个变量试图同时访问或修改相同内存导致的冲突或错误。编译器强制执行这一规则,确保内存和线程的安全。

例如:

fn main() {
    let x = vec![1, 2, 3];
    take_ownership(x);
    take_ownership(x);
    // error: use of moved value: `x`
}

fn take_ownership(v: Vec<i32>) {
    // 对 v 做些事情
    println!("{:?}", v);
}

如果我们运行这段代码,我们会得到编译器给出的以下(非常有用的)错误信息:

error[E0382]: use of moved value: `x`
 --> src/main.rs:4:20
  |
2 |     let x = vec![1, 2, 3];
  |         - move occurs because `x` has type `Vec<i32>`, which does not implement the `Copy` trait
3 |     take_ownership(x);
  |                    - value moved here
4 |     take_ownership(x);
  |                    ^ value used here after move

For more information about this error, try `rustc --explain E0382`.

因为第一次函数调用取得了变量 x 的所有权,第二次函数调用违反了第二条规则:只能有一个所有者。想象如果一个指向向量的指针被传递给两个函数,那么编译器将无法安全地释放内存。

那么关于 Copy 特性的这条注释呢?

如果我们用整数而不是向量尝试同样的操作:

fn main() {
    let x = 42;
    take_ownership(x);
    take_ownership(x);
    // error: use of moved value: `x`
}

fn take_ownership(i: i32) {
    // 对 i 做些事情
    println!("{i}");
}

运行这段代码实际上可以正常工作,并生成以下输出:

42
42

为什么编译器没有对违反第二条规则提出抱怨呢?如果一个值可以适合单个机器字(即在 64 位架构上为 8 字节),Rust 编译器可以廉价地复制整个数据。数据本身不大于指向数据的指针。因为数据被完全复制,接收范围可以在使用完该值后简单地释放内存。

3. 当所有者超出作用域时值被丢弃

虽然第二条规则解决了访问已释放内存或可能两次释放内存的问题,第三条规则防止了内存泄漏,即内存永远不被释放。程序消耗的资源量随着时间的推移而增加。

在 Rust 中,变量和值有一个作用域,即程序中变量可见和可访问的部分。当一个变量超出作用域时,关联的值会自动被丢弃,内存被释放。这确保了内存不会泄漏,程序中没有内存泄漏。

例如:

fn main() {
    // 新作用域
    {
        let x = 42;
        println!("{}", x);
    } // x 在这里超出作用域
    println!("{x} 在这里不可访问");
}

运行这段代码会产生以下错误:

error[E0425]: cannot find value `x` in this scope
 --> src/main.rs:7:16
  |
7 |     println!("{x} 在这里不可访问");
  |                ^ not found in this scope

For more information about this error, try `rustc --explain E0425`.

通过强制执行这些规则,Rust 的所有权系统可以在没有运行时或垃圾收集器的情况下防止与手动内存管理相关的常见错误。

借 用

Rust 的所有权系统还允许借用,即在不取得所有权的情况下在特定作用域内借用一个值。这意味着多个变量可以读取和访问该值而不取得所有权,从而提供一种更安全和受控的数据使用和操作方式。

在 Rust 中借用变量使用 & 符号位于变量名前。这被称为引用。被借用的变量仍然是所有者,借用者只能读取其值。借用者没有变量的所有权权利,因此不能修改它。

例如:

fn main() {
    let s = "hello";
    // 传递一个引用
    print_length(&s);
    // 我们仍然可以在这里使用 s
    println!("{}", s);
}

fn print_length(s: &str) {
    println!("'{}' 的长度是 {}", s, s.len());
}

在这个例子中,我们将变量 s 的引用传递给函数 print_length。这个函数可以读取 s 的值但不能修改它。变量 s 在主函数中仍然可访问和使用。这允许我们在代码的多个部分使用变量而不必每次都转移所有权。

还可以创建可变引用,允许多个变量读取和修改一个值。这是一种有用的模式,但也有一些限制,例如同时只能有一个可变借用。这意味着当变量被可变借用时,原始变量不能再次被借用。

例如:

fn main() {
    let mut s = String::from("Hello");
    add_word(&mut s);
    println!("{}", s); // 输出 "Hello world"
}

fn add_word(s: &mut String) {
    s.push_str(" world");
}

结 论

Rust 的所有权系统是确保程序安全和高效使用内存的关键特性。所有权系统强制执行三条主要规则:每个值都有一个所有者,只能有一个所有者,当所有者超出作用域时值被丢弃。这些规则有助于防止常见错误,如数据竞争和段错误。

所有权系统还允许借用值,允许多个变量读取一个值而不取得其所有权。这为程序中的内存使用提供了更多的灵活性和控制。

对于新手 Rust 程序员来说,所有权系统可能是一条陡峭的学习曲线,但它最终通过防止常见的内存相关错误帮助 Rust 程序更快、更安全。相比于依赖垃圾回收或手动内存管理的其他语言,这可能是一个显著的优势。

我强烈鼓励读者亲自尝试 Rust 并体验所有权系统的好处。随着你对 Rust 经验的增长,编写安全高效代码会变得更加自然。